<?php
require_once Config::ROOT . '/libs/advmodule/Presentation.php';
$presentations = glob(Config::ROOT . '/libs/advmodule/Presentation*.php');
foreach ($presentations as $presentation) {
    require_once $presentation;
}

/**
 * Advanced module - uses automatic administration interface generated from
 * descriptions of the data model.
 * 
 * Abilities:
 *  - automatic administration UI generating (when the user right 'view admin'
 *    is defined)
 *  - file uploads handling
 *  - automatic switching between compressed/uncompressed JS files
 *  - record pre-persist event handlers
 *  - automatic module metadata generating
 *  - support for .ini language files
 * 
 * Each DB table should be defined like this:
 *     const TABLE_NAME = 'items';
 *     protected $items =
 *             array('id' => 'iPN+', 'title' => 'sN', 'note' => 's');
 *     protected $itemPresentation = array(
 *         'id' => 'someType',
 *         'title' => array(
 *             'somePresentationType',
 *             'anotherType'
 *         )
 *         // no need to specify presentation of 'note', default is used
 *     );
 *     protected $itemRecord;
 * 
 * There are some required properties:
 *     protected $filesRoot; // must point to the file category ID for storing
 *                           // files related to the module
 * 
 * Records and persisted automatically. To manipulate record data simply define
 * a pre-persist event handler like this:
 * 
 *     public function onPreItem(&$record, $source) {
 *         // manipulate the $record before persisting it to the database
 *     }
 * 
 * The module may optionaly define module's settings:
 *     protected $settings = array(
 *         'someProperty' => new PresentationSomeType(),
 *         'anotherProperty' => array(
 *             'someType',
 *             'antoherType'
 *         )
 *     );
 *     protected $someProperty; // settings properties must be defined
 *     protected $anotherProperty;
 */
abstract class AdvModule extends Module {
    protected $details = null;
    protected $adminItemsOnPage = 20;
    
    public function __construct(DBAPI $dbapi, RecordList $recordList) {
        $reflection = new ReflectionObject($this);
        $this->name = $reflection->getName();
        $this->administrable = array_search('view admin',
                $this->getRights()) !== false;
        $this->version = date('Y-m-d', filectime(__FILE__));
        if ($this->details === null) {
            $this->details = array('dependencies' => array(),
                    'observes' => array($this->name));
        }
        $constants = $reflection->getConstants();
        foreach ($constants as $constant => $value) {
            if (substr($constant, 0, 4) == 'MSG_') {
                continue;
            }
            $recordName = substr($value, 0, -1) . 'Record';
            $this->{$recordName} = array('table' => $value, 'data' => array(),
                    'properties' => $this->{$value});
        }
        $this->loadSettings();
        $this->transformPresentationsToArrays();
        parent::__construct($dbapi, $recordList);
        $this->initPresentations();
    }
    
    public function getDetails() {
        if (!isset($this->details['localization']) ||
                (count($this->details['localization']) == 0)) {
            $loc1 = glob($this->getDir('/lang/??.[ip][nh][ip]'));
            $loc1 = $loc1 ? $loc1 : array();
            $loc2 = glob($this->getDir('/lang/??_??.[ip][nh][ip]'));
            $loc2 = $loc2 ? $loc2 : array();
            $this->details['localization'] = array_merge($loc1, $loc2);
            foreach($this->details['localization'] as &$locale) {
                $locale = substr($locale, strrpos($locale, '/') + 1);
                $locale = substr($locale, 0, strpos($locale, '.'));
            }
        }
        return $this->details;
    }
    
    public function install() {
        $reflection = new ReflectionObject($this);
        $constants = $reflection->getConstants();
        Modules::getModule('Users')->registerRights($this->getName(),
                $this->getRights());
        FTP::createFile($this->getDir('/settings', true), 0666);
        file_put_contents($this->getDir('/settings'), serialize(array()));
        foreach ($constants as $constant => $value) {
            if (substr($constant, 0, 4) == 'MSG_') {
                continue;
            }
            if (!$this->dbdriver->createTable($value, $this->{$value})) {
                return false;
            }
        }
        return true;
    }
    
    public function uninstall() {
        $reflection = new ReflectionObject($this);
        $constants = $reflection->getConstants();
        foreach ($constants as $constant => $value) {
            if (substr($constant, 0, 4) == 'MSG_') {
                continue;
            }
            if (!$this->dbdriver->dropTable($value)) {
                return false;
            }
        }
        FTP::deleteFile($this->getDir('/settings', true));
        return true;
    }
    
    public function js($path, $relative = true) {
        if ($relative) {
            $path = Config::WEB_DIR . $this->getDir($path, true);
        }
        $path = substr($path, strlen(Config::WEB_DIR));
        $min = preg_replace('/[.]js$/', '.min.js', $path);
        if (file_exists(Config::ROOT . $min)) {
            if (filectime(Config::ROOT . $min) >
                    filectime(Config::ROOT . $path)) {
                return parent::js(Config::WEB_DIR . $min, false);
            }
        }
        parent::js(Config::WEB_DIR . $path, false);
    }
    
    public function initDisplay($param = null) {
        parent::initDisplay($param);
        if (Kernel::getMode() == Kernel::ADMIN) {
            $this->js('/utils/advmodule/admin.js', false);
            $url = Modules::getModule('View')->getUrl();
            if (isset($url[1]) && preg_match('/^(add|edit)-(.+)$/', $url[1],
                    $match)) {
                $presentationName = substr($match[2], 0, -1) . 'Presentation';
                $presentation = $this->{$presentationName};
                foreach ($presentation as $field => $parameters) {
                    foreach ($presentation[$field] as $fieldPresentation) {
                        $fieldPresentation->initFormDisplay();
                    }
                }
            }
            if (isset($url[1]) && ($url[1] == 'settings')) {
                foreach ($this->settings as $field => $presentation) {
                    foreach ($presentation as $fieldPresentation) {
                        $fieldPresentation->initFormDisplay();
                    }
                }
            }
        }
    }

    public function displayAdminIndex($page = 1) {
        $reflection = new ReflectionObject($this);
        $constants = $reflection->getConstants();
        foreach ($constants as $constant => $value) {
            if (substr($constant, 0, 4) == 'MSG_') {
                continue;
            }
            $this->displayList($value, $page, true);
            return;
        }
    }
    
    public function displayBlank($param = null) {
        if (Kernel::getMode() == Kernel::ADMIN) {
            $url = Modules::getModule('View')->getUrl();
            if (isset($url[1]) && preg_match('/^add-(.+)$/', $url[1], $match)) {
                array_splice($url, 0, 2);
                $this->displayAdd($match[1], $url);
                return;
            }
            if (isset($url[1]) && preg_match('/^edit-(.+)$/', $url[1],
                    $match)) {
                array_splice($url, 0, 2);
                $this->displayEdit($match[1], $url);
                return;
            }
            if (isset($url[1]) && preg_match('/^list-(.+)$/', $url[1],
                    $match)) {
                array_splice($url, 0, 2);
                $this->displayList($match[1], isset($url[0]) ?
                        (int) $url[0] : 1);
            }
        }
    }
    
    public function displayAdminSubmenu() {
        $menu = array();
        $reflection = new ReflectionObject($this);
        $constants = $reflection->getConstants();
        $first = true;
        $url = Modules::getModule('View')->getUrl();
        $datasetUrl = isset($url[1]) ? "{$url[1]}/" : '';
        foreach ($constants as $constant => $value) {
            if (substr($constant, 0, 4) == 'MSG_') {
                continue;
            }
            $menu[] = array('label' => $value, 'href' => $first ? '' :
                    'list-' . $value . '/');
            $menu[] = array('label' => 'add' . ucfirst(substr($value, 0, -1)),
                    'href' => 'add-' . $value . '/');
            $first = false;
        }
        foreach ($menu as &$item) {
            $item['active'] = $item['href'] == $datasetUrl;
        }
        try {
            $property = $reflection->getProperty('settings');
            $menu[] = array('label' => 'settings', 'href' => 'settings/');
        } catch (Exception $e) {}
        $this->prepareMenu($menu);
        $params = array('menu' => $menu);
        $this->template('submenu', $params);
    }
    
    public function displayAdminSettings() {
        $form = array('start' => 'settings', 'legend' => 'settingsForm',
                'submit' => 'save', 'items' => array());
        foreach ($this->settings as $settings => $specs) {
            $form['items'][] = array('label' => $settings, 'name' => $settings,
                    'value' => $this->{$settings});
        }
        $this->formatFormFields($form, 'settings', $this->settings);
        $this->displayForm($form);
    }
    
    public function onAdd(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        if (!$this->hasRights('view admin')) {
            return;
        }
        $table = $details['_tableName'];
        $tables = $this->getTables();
        if (isset($tables[0]) && ($tables[0] != $table)) {
            Kernel::setRedirect($this->getLink('list-' . $table . '/'));
        }
        $recordName = substr($table, 0, -1) . 'Record';
        $record = $this->{$recordName};
        $record = $this->fillRecord($record, $details);
        $presentationName = substr($table, 0, -1) . 'Presentation';
        $presentation = $this->{$presentationName};
        $this->fixInputByPresentation($record, $presentation, $details);
        $this->processFiles($record);
        $this->executePrePersistHandler($record);
        $this->dbapi->save($record);
    }
    
    public function onEdit(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        if (!$this->hasRights('view admin')) {
            return;
        }
        $table = $details['_tableName'];
        $tables = $this->getTables();
        if (isset($tables[0]) && ($tables[0] != $table)) {
            Kernel::setRedirect($this->getLink('list-' . $table . '/'));
        }
        $recordName = substr($table, 0, -1) . 'Record';
        $record = $this->{$recordName};
        $record['data'][$this->findTablePrimaryKey($table)] =
                $details[$this->findTablePrimaryKey($table)];
        $this->dbapi->refresh($record);
        $record = $this->fillRecord($record, $details);
        $presentationName = substr($table, 0, -1) . 'Presentation';
        $presentation = $this->{$presentationName};
        $this->fixInputByPresentation($record, $presentation, $details);
        $this->processFiles($record);
        $this->executePrePersistHandler($record);
        $this->dbapi->update($record);
    }
    
    public function onDelete(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        if (!$this->hasRights('view admin')) {
            return;
        }
        $table = substr($details, 0, strrpos($details, '-'));
        $id = substr($details, strrpos($details, '-') + 1);
        $this->dbdriver->delete($table,
                '`' . $this->findTablePrimaryKey($table) . '` = ' . (int) $id);
    }
    
    public function onDeleteAll(&$details, $source) {
        Kernel::setRedirect($this->getLink());
        if (!$this->hasRights('view admin')) {
            return;
        }
        if (!isset($details['id']) || !is_array($details['id'])) {
            return;
        }
        $ids = array();
        foreach ($details['id'] as $id) {
            $ids[] = (int) $id;
        }
        $table = $details['_tableName'];
        $primaryKey = $this->findTablePrimaryKey($table);
        $this->dbdriver->delete($table, "`$primaryKey` IN (" .
                implode(', ', $ids) . ")");
    }
    
    public function onSettings(&$details, $source) {
        foreach ($this->settings as $property => $specs) {
            $this->{$property} = isset($details[$property]) ?
                    $details[$property] : null;
        }
        $this->saveSettings();
        Kernel::setRedirect($this->getLink());
    }
    
    protected function loadLang($localization) {
        $file = $this->getDir("/lang/$localization");
        if (file_exists("$file.ini")) {
            $this->langArray = parse_ini_file("$file.ini");
            return;
        }
        $lang = array();
        require_once $this->getDir("$file.php");
        $this->langArray = $lang;
    }
    
    protected function displayEdit($tableName, $params) {
        $recordName = substr($tableName, 0, -1) . 'Record';
        $record = $this->{$recordName};
        $record['data'][$this->findTablePrimaryKey($tableName)] =
                (int) $params[0];
        $this->dbapi->refresh($record);
        $form = $this->createEditForm('edit', $record);
        $form['items'][] = array('name' => '_tableName', 'value' => $tableName,
                'type' => 'hidden', 'label' => '');
        $form['legend'] = 'edit' . ucfirst($tableName) . 'Form';
        $presentationName = substr($tableName, 0, -1) . 'Presentation';
        $presentation = $this->{$presentationName};
        $this->formatFormFields($form, $tableName, $presentation);
        $this->displayForm($form);
    }
    
    protected function displayAdd($tableName, $params) {
        $form = $this->createAddForm('add', $this->{$tableName});
        $form['items'][] = array('name' => '_tableName', 'value' => $tableName,
                'type' => 'hidden', 'label' => '');
        $form['legend'] = 'add' . ucfirst($tableName) . 'Form';
        $presentationName = substr($tableName, 0, -1) . 'Presentation';
        $presentation = $this->{$presentationName};
        $this->formatFormFields($form, $tableName, $presentation);
        $this->displayForm($form);
    }
    
    protected function displayList($tableName, $page = 1, $index = false) {
        $table = $this->{$tableName};
        $presentationName = substr($tableName, 0, -1) . 'Presentation';
        $presentation = $this->{$presentationName};
        $count = $this->getRecordsCount($tableName);
        $pages = ceil($count / $this->adminItemsOnPage);
        $page = $page > $pages ? $pages : $page;
        $page = $page < 1 ? 1 : $page;
        $primaryKey = $this->findTablePrimaryKey($tableName);
        $params = array('head' => array(array()), 'items' => array(),
                'table' => $this->getFilteringPresentation($table,
                $presentation), 'tableName' => $tableName, 'form' =>
                $this->getForm('deleteAll', true, $this->getName()),
                'paging' => $this->getPagingParams($page, $pages, $tableName),
                'filter' => isset($_GET['filter']) ? $_GET['filter'] : array());
        $this->createListHead($params, $table, $tableName, $presentation);
        $cols = array();
        $order = array('!' . $primaryKey);
        $data = $this->dbdriver->select($tableName, $cols,
                $this->getFilteringSQL(), $order, ($page - 1) *
                $this->adminItemsOnPage, $this->adminItemsOnPage);
        $this->createListContent($data, $primaryKey, $presentation, $tableName,
                $params, $table);
        $this->template('advmodule/admin-list', $params);
    }
    
    protected function lang($key, &$params = array()) {
        if (isset($this->langArray[$key])) {
            return parent::lang($key, $params);
        } else {
            return "???$key???";
        }
    }
    
    protected function getRecordsCount($tableName) {
        $count = array('COUNT(*)');
        $count = $this->dbdriver->select($tableName, $count);
        return $count[0]['COUNT(*)'];
    }
    
    private function getFilteringSQL() {
        if (!isset($_GET['filter']) || !is_array($_GET['filter'])) {
            return '';
        }
        $sql = '';
        foreach ($_GET['filter'] as $property => $value) {
            $sql .= ($sql ? ' AND ' : '') . "`" .
                    $this->dbdriver->escapeString($property) . "` LIKE '%" .
                    $this->dbdriver->escapeString($value) . "%'";
        }
        return $sql;
    }
    
    private function getFilteringPresentation($table, $presentation) {
        $structure = $this->getDisplayedTableStructure($table, $presentation);
        foreach ($structure as $field => &$specs) {
            if (isset($presentation[$field])) {
                foreach ($presentation[$field] as $fieldPresentation) {
                    $specs = $fieldPresentation->getFilteringType();
                }
            }
        }
        return $structure;
    }
    
    private function getDisplayedTableStructure($table, $presentation) {
        $displayed = array();
        foreach ($table as $key => $type) {
            if (!$this->isHiddenInList($key, $presentation)) {
                $displayed[$key] = $type;
            }
        }
        return $displayed;
    }
    
    private function executePrePersistHandler(&$record) {
        $tableName = $record['table'];
        $event = 'pre' . ucfirst(substr($tableName, 0, -1));
        $handlerName = 'on' . ucfirst($event);
        try {
            $reflection = new ReflectionObject($this);
            $handler = $reflection->getMethod($handlerName);
            return $this->{$handlerName}($record, $this->getName());
        } catch (Exception $e) {}
        return true;
    }
    
    private function processFiles(&$record) {
        if (!empty($_FILES) && isset($this->filesRoot) && $this->filesRoot) {
            $files = &Modules::getModule('Files')->uploadFiles($_FILES,
                    $this->filesRoot);
            foreach ($_FILES as $fileProperty => &$file) {
                foreach ($files as &$fileRecord) {
                    if ($file['name'] == $fileRecord['data']['name']) {
                        $record['data'][$fileProperty] =
                                $fileRecord['data']['id'];
                    }
                }
            }
        }
    }
    
    private function formatFormFields(&$form, $tableName, &$presentation) {
        foreach ($form['items'] as &$item) {
            if (!isset($item['label'])) {
                continue;
            }
            $item['label'] = $tableName . ucfirst($item['label']) . 'Form';
            $this->formatFormField($item, $presentation);
        }
    }
    
    private function getPagingParams($page, $pages, $tableName) {
        $reflection = new ReflectionObject($this);
        $constants = $reflection->getConstants();
        $link = "list-$tableName";
        foreach ($constants as $value) {
            if ($tableName == $value) {
                $link = 'index';
            }
            break;
        }
        $suffix = '/';
        if (isset($_GET['filter']) && is_array($_GET['filter'])) {
            $filter = '';
            foreach ($_GET['filter'] as $field => $value) {
                $filter .= ($filter ? '&amp;' : '') . "filter[$field]=$value";
            }
            $suffix .= "?$filter";
        }
        return array('linkPreffix' => $this->getLink("$link/"),
                'linkSuffix' => $suffix, 'current' => $page, 'count' => $pages);
    }
    
    private function fixInputByPresentation(&$record, &$presentation, $data) {
        foreach ($presentation as $property => $propertyPresentations) {
            foreach ($propertyPresentations as $propertyPresentation) {
                $record['data'][$property] = $propertyPresentation->
                        fixInput(isset($record['data'][$property]) ?
                        $record['data'][$property] : null,
                        isset($data[$property]) ? $data[$property] : null);
            }
        }
    }
    
    private function formatFormField(&$field, &$presentation) {
        if (isset($presentation[$field['name']])) {
            foreach ($presentation[$field['name']] as $fieldPresentation) {
                $fieldPresentation->formatFormField($field);
            }
        }
    }
    
    private function formatValueForList($value, $property, &$presentation) {
        if (isset($presentation[$property])) {
            foreach ($presentation[$property] as $fieldPresentation) {
                $value = $fieldPresentation->formatForList($value);
            }
        }
        return $value;
    }
    
    private function loadSettings() {
        if (!file_exists($this->getDir('/settings'))) {
            return;
        }
        $settings = unserialize(file_get_contents($this->getDir('/settings')));
        foreach ($settings as $property => $value) {
            $this->{$property} = $value;
        }
    }
    
    private function saveSettings() {
        if (!file_exists($this->getDir('/settings'))) {
            return;
        }
        $settings = array();
        foreach ($this->settings as $property => $specs) {
            $settings[$property] = $this->{$property};
        }
        file_put_contents($this->getDir('/settings'), serialize($settings));
    }
    
    private function createListContent(&$data, $primaryKey, &$presentation,
            $tableName, &$params, &$table) {
        foreach ($data as $row) {
            $tableData = array();
            $tableData['id'] = $row[$primaryKey];
            foreach ($table as $rowName => $specs) {
                if ($this->isHiddenInList($rowName, $presentation)) {
                    continue;
                }
                $tableData[] = $this->formatValueForList($row[$rowName],
                        $rowName, $presentation);
            }
            $tableData['edit'] = $this->getLink("edit-$tableName/" .
                    $row[$primaryKey] . '/');
            $tableData['delete'] = $this->getLink("$tableName-" .
                    $row[$primaryKey], 'delete');
            $params['items'][] = $tableData;
        }
    }
    
    private function createListHead(&$params, &$table, &$tableName,
            &$presentation) {
        foreach ($table as $row => $type) {
            if ($this->isHiddenInList($row, $presentation)) {
                continue;
            }
            $params['head'][0][] = $this->lang($tableName . ucfirst($row));
        }
    }
    
    private function findTablePrimaryKey($tableName) {
        $table = $this->{$tableName};
        foreach ($table as $row => $specs) {
            if (strpos($specs, 'P') !== false) {
                return $row;
            }
        }
        foreach ($table as $row => $specs) {
            return $row;
        }
        return '';
    }
    
    private function isHiddenInList($field, $presentation) {
        if (!isset($presentation[$field])) {
            return false;
        }
        foreach ($presentation[$field] as $fieldPresentation) {
            if ($fieldPresentation->isHiddenInList()) {
                return true;
            }
        }
        return false;
    }
    
    private function transformPresentationsToArrays() {
        $tables = $this->getTables();
        foreach ($tables as $table) {
            $presentation = substr($table, 0, -1) . 'Presentation';
            $presentationData = &$this->{$presentation};
            foreach ($presentationData as &$fieldPresentation) {
                if (!is_array($fieldPresentation)) {
                    $fieldPresentation = array($fieldPresentation);
                }
            }
            //$this->{$presentation} = $presentationData;
        }
        if (isset($this->settings)) {
            foreach ($this->settings as &$presentations) {
                if (!is_array($presentations)) {
                    $presentations = array($presentations);
                }
            }
        }
    }
    
    private function initPresentations() {
        $tables = $this->getTables();
        foreach ($tables as $table) {
            $presentation = substr($table, 0, -1) . 'Presentation';
            $presentationData = &$this->{$presentation};
            foreach ($presentationData as &$fieldPresentations) {
                foreach ($fieldPresentations as &$fieldPresentation) {
                    $fieldPresentation = explode(':', $fieldPresentation);
                    $fieldPresentationClass =
                            'Presentation' . ucfirst($fieldPresentation[0]);
                    array_splice($fieldPresentation, 0, 1);
                    $reflection = new ReflectionClass($fieldPresentationClass);
                    $instance = empty($fieldPresentation) ?
                            $reflection->newInstance() :
                            $reflection->newInstanceArgs($fieldPresentation);
                    $fieldPresentation = $instance;
                    $fieldPresentation->setDBDriver($this->dbdriver);
                    $fieldPresentation->setLangArray($this->langArray);
                }
            }
        }
        if (isset($this->settings)) {
            foreach ($this->settings as &$presentations) {
                foreach ($presentations as &$presentation) {
                    $presentation = explode(':', $presentation);
                    $presentationClass =
                            'Presentation' . ucfirst($presentation[0]);
                    array_splice($presentation, 0, 1);
                    $reflection = new ReflectionClass($presentationClass);
                    $instance = empty($presentation) ?
                            $reflection->newInstance() :
                            $reflection->newInstanceArgs($presentation);
                    $presentation = $instance;
                    $presentation->setDBDriver($this->dbdriver);
                    $presentation->setLangArray($this->langArray);
                }
            }
        }
    }
    
    private function getTables() {
        $reflection = new ReflectionObject($this);
        $constants = $reflection->getConstants();
        $tables = array();
        foreach ($constants as $constant => $value) {
            if (substr($constant, 0, 4) == 'MSG_') {
                continue;
            }
            $tables[] = $value;
        }
        return $tables;
    }
    
    protected function prepareFormData(&$form) {
        parent::prepareFormData($form);
        foreach ($form['items'] as &$item) {
            if ($item['type'] == 'html') {
                $item['html'] = parent::lang($item['html']);
            }
        }
    }
}
